<?php
/**
 * scottlib ORM
 * @author Aaron Collegeman aaroncollegeman.com
 * @since 0.1.3
 *
 * Copyright (C)2008 Collegeman.net, LLC.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 */

// ----------------------------------------------------------------------------
// Don't touch anything in this file.  That way when there's an update, you
// can just overwrite this file with the new one.  Go ahead, we warned you!
// ----------------------------------------------------------------------------

/**
 * Largely for the purpose of debugging, many of the methods of the scottlib ORM
 * and data bucket classes support generating and returning the SQL they generate,
 * instead of executing it.
 */
if (!defined('SCOTTLIB_RETURN_SQL'))
	define('SCOTTLIB_RETURN_SQL', false);

/**
 * The scottlib ORM domain baseclass.  Magic.
 *
 * @since 0.1.0
 */
abstract class slObject {

	/** The name of the underlying table, discovered via annotation "table". */
	private $_table;
	
	/** The name of the id field, discovered property annotation "id". */
	private $_idProperty;
	
	/** Validation errors. */
	private $_errors = array();
	
	/** Object relationship state - references to objects defined and loaded via ORM logic. */
	private $_state;
	
	/** Annotation cache. */
	private static $_annotations = array();
	
	/** Reference to slMySqlConn instance. */
	private $_conn;
	
	/** 
	 * default Id property 
	 * @transient
	 */
	public $id;
	
	/**
	 * Constructor.
	 * @param mixed $stateOrId To create an object with initial state, use an array; to dynamically load a known instance, use a primary key value.
	 * @param slMySqlConn &$conn An initialized instance of slMySqlConn, or null; when null, slObject will use slMySQLConn::instance() to access the most recently connected instance.
	 */
	function __construct($stateOrId = null, slMySqlConn &$conn = null) {
		$this->_table = ReflectionUtils::getClassAnnotation($this, 'table');
		if (!$this->_table)
			$this->_table = slObject::uncap(get_class($this));
		
		$this->_idProperty = self::getIdPropertyName($this);
		
		// grab connection object:
		$this->_conn = ($conn != null ? $conn : slMySQLConn::instance());	
			
		if ($stateOrId != null) {
			if (is_array($stateOrId) && count($stateOrId))
				$this->bindAndValidate($stateOrId);
			else
				$this->load($stateOrId);
		}	
	}
	
	/**
	 * *** This function will be fully supported in PHP 5.3.0.
	 * Instantiates and loads an instance of the calling class.
	 * In PHP version < 5.3.0, you must provide the class name as the second parameter.
	 */
	final static function get($id, $className = null) {
		if (function_exists('get_called_class')) {
			$class = get_called_class();
			$obj = new $class();
		}
		else if ($className) {
			$obj = new $className();
		}
		else {
			throw new Exception('Either PHP version must be >= 5.3.0, or you must provide a value for $className parameter');	
		}
		
		$obj->load($id);
		return $obj;
	}
	
	/**
 	 * *** This function will be fully supported in PHP 5.3.0.
	 * @return the total number of records
	 */
	final static function count(slObject $obj) {
		$conn = $obj->getConn();
		return $conn->val(slMySqlConn::prepare('SELECT COUNT(#) FROM #', array($obj->getIdProperty(), $obj->getTableName()))); 		
	}
	
	/**
	 * @return The first ReflectionProperty object found to have the annotation "id"; if not found, assumed to be "id"
	 */
	private static function getIdPropertyName(slObject $object) {
		$cache = ReflectionUtils::getCachedReflection($object);
		foreach($cache['properties'] as $property) {
			if (ReflectionUtils::getPropertyAnnotation($object, $property->getName(), 'id') !== false)
				return $property->getName();
		}
		
		// default id property name:
		return "id";
	}
	
	public function getId() {
		$idProperty = $this->_idProperty;
		return $this->$idProperty;	
	}
	
	public function setId($id) {
		$idProperty = $this->_idProperty;
		$this->$idProperty = $id;
	}
	
	/**
 	 * Persist all data attached to this object.
	 * @return When SCOTTLIB_RETURN_SQL is true, method returns the text of the SQL statements for executing the save action; 
	 * otherwise, the method executes the SQL and returns the number of records affected by the save.
	 */
	final function save() {
		$return = SCOTTLIB_RETURN_SQL;
		
		$names = array();
		$values = array();
		$types = array();
		
		$id = $this->getId();
		
		if ($id)
			$this->onBeforeUpdate($id);
		else
			$this->onBeforeInsert();
		
		if (!$this->validate()) 
			return false;
			
		foreach($this->getProperties() as $prop) {
			if (get_class($prop) == 'ReflectionProperty' && $prop->getName() != $this->_idProperty) {
				
				if (!ReflectionUtils::hasPropertyAnnotation($this, $prop->getName(), array('subquery', 'transient'))) {
					
					$name = $prop->getName();
					$value = $this->$name;

					if (ReflectionUtils::hasPropertyAnnotation($this, $name, 'notNull') && $value === null)
						throw new Exception("Property `$name` is not allowed to be null.");
					
					
					if (ReflectionUtils::hasPropertyAnnotation($this, $name, 'integer')) {
						if (strlen($value) && ( !is_numeric($value) || stripos($value, '.') !== false ) )
							throw new Exception("Property `$name` must be an integer: found [$value]");
						else if (!$value && $value !== 0)
							$value = null;
							
						$types[] = '%d';
					}
					
					else if (ReflectionUtils::hasPropertyAnnotation($this, $name, 'float') || ReflectionUtils::hasPropertyAnnotation($this, $name, 'double')) {
						if ($value && !is_numeric($value))
							throw new Exception("Property `$name` must be a number: found [$value]");
						else if (!$value && $value !== 0)
							$value = null;

						$types[] = '%d';
					}
					
					else if (ReflectionUtils::hasPropertyAnnotation($this, $name, 'date')) {
						if (is_numeric($value))
							$value = date('c', $value);
						
						if ($value !== null && strtotime($value) === false)
							throw new Exception("Property `$name` must be a date: found [$value]");

							
						// auto timestamping:
						if (($name == 'dateCreated' || $name == 'lastUpdated') && (!ReflectionUtils::hasClassAnnotation($this, 'autoTimestamp') || ReflectionUtils::getClassAnnotation($this, 'autoTimestamp') != 'false')) {
							if (!$this->getId()) 
								$value = time();	
							else if ($name == 'lastUpdated') {
								$value = time();	
							}
							$this->$name = date('Y/m/d H:i:s', $value);
						}
							
						$value = date('Y/m/d H:i:s', (!is_numeric($value) ? strtotime($value) : $value));
							
						$types[] = '%s'; // don't use %c here, because %c will force the prepare method to repeat the date translation we just did above...
					}
					
					else if (ReflectionUtils::hasPropertyAnnotation($this, $name, 'boolean') || ReflectionUtils::hasPropertyAnnotation($this, $name, 'bool')) {
						
						$types[] = '%b';	
						
					}
					
					else {
						if (ReflectionUtils::hasPropertyAnnotation($this, $name, 'serialized'))
							$value = serialize($value);	
							
						$types[] = '%s';
					}
					
					
					// last, if @fieldName is present, use instead of $name
					if ($fieldName = ReflectionUtils::getPropertyAnnotation($this, $name, 'fieldName'))
						$name = $fieldName;
					
					$names[] = $name;
					$values[] = $value;
				}
			}
		}
		
		// echo '<pre>'.print_r(array($names, $values, $types), true).'</pre>';
		
		if ($id) { // update
			
			$placeholders = array();
			$placeholderValues = array();
			foreach($names as $i => $name) { 
				$placeholders[] = "# = $types[$i]";
				$placeholderValues[] = $name;
				$placeholderValues[] = $values[$i];
			}
			
			$raw = "UPDATE # SET ".join(', ', $placeholders)." WHERE #=%s";
			$sql = slMySqlConn::prepare($raw, array_merge(array($this->_table), $placeholderValues, array($this->_idProperty), array($id))); 
		
			if ($return)
				return $sql;
			else {	
				$affected = $this->_conn->update($sql);
				$this->onAfterUpdate($id);
				return $affected;
			}
			
		}
		
		else { // insert
			
			$namePlacs = array();
			foreach($names as $n)
				$namePlacs[] = "#";
				
			$raw = "INSERT INTO # (".join(', ', $namePlacs).") VALUES (".join(', ', $types).")";
			$sql = slMySqlConn::prepare($raw, array_merge(array($this->_table), $names, $values));

			if ($return)
				return $sql;
			else {
				$affected = $this->_conn->insert($sql);
				$this->setId($this->_conn->val('SELECT @@IDENTITY'));
				$this->onAfterInsert($this->getId());
				return $affected;
			}
			
		}
		
	}
	
	function onBeforeSave($id) {}
	function onAfterSave($id) {}
	function onBeforeInsert() { $this->onBeforeSave(null); }
	function onAfterInsert($id) { $this->onAfterSave($id); }
	function onBeforeUpdate($id) { $this->onBeforeSave($id); }
	function onAfterUpdate($id) { $this->onAfterSave($id); } 
	
	/**
 	 * Accessor magic method, used to alias ORM accessor methods, e.g., $object->foo executes $object->getFoo() 
	 */
	final function __get($name) {
		
	}
	
	/**
	 * Mutator magic method, used to alias ORM mutator methods, e.g., $object->foo = 'value' execute $object->setFoo('value')
	 */
	final function __set($name, $value) {
		
	}
	
	/**
	 * @return Array containing the ReflectionProperty objects that represent the state of this object.
	 */
	private function getProperties() {
		$cache = ReflectionUtils::getCachedReflection($this);
		return $cache['properties'];
	}
	
	/**
	 * @return The cached ReflectionClass object for $this
	 */		
	private function getClass() {
		$cache = ReflectionUtils::getCachedReflection($this);
		return $cache['class'];
	}
	
	public function getTableName() {
		return $this->_table;	
	}
	
	private function getConn() {
		return $this->_conn;	
	}
	
	protected function getIdProperty() {
		return $this->_idProperty;	
	}
	
	/**
	 * @return String, an uncapitalized version of $name
	 */
	private static function uncap($name) {
		return ($name != null) ? strtolower(substr($name, 0, 1)).substr($name, 1) : null;
	}
	
	/**
	 * @return String, a capitalized version of $name
	 */
	private static function cap($name) {
		return ($name != null) ? strtoupper(substr($name, 0, 1)).substr($name, 1) : null;
	}
	
	/**
	 * Executes query with WHERE and ORDER phrases from $where_and_order, inserting parameters $params, offsetting and limiting with $limit and $offset.
	 * When SCOTTLIB_RETURN_SQL is true, returns the DQL rather than executing it.
	 * @param string $where_and_order
	 * @param Array $params
	 * @param int $limit
	 * @param int $offset
	 * @return When records are found, an Array of instantiated and populated instance of your object; otherwise, an empty Array. This, unless
	 * SCOTTLIB_RETURN_SQL, in which case the DQL is returned instead.
	 */
	final function select($where_and_order, $params = array(), $limit = 10, $offset = 0) {
		$return = SCOTTLIB_RETURN_SQL;
		
		$class = $this->getClass();
		
		$subqueries = array();
		$subqueries[] = '*';
		
		foreach($this->getProperties() as $prop) {
			if (get_class($prop) == 'ReflectionProperty') {
				if ($q = ReflectionUtils::getPropertyAnnotation($this, $prop->getName(), 'subquery')) {
					$prepared = slMySqlConn::prepare($q, array('tableName' => $this->getTableName()));
					$sql = slMySqlConn::prepare("( %p ) AS #", array($prepared, $prop->getName()));
					$subqueries[] = $sql; 
				}
			}
		}
		
		$sql = slMySqlConn::prepare("SELECT %p FROM # %p LIMIT %d OFFSET %d", array(join(',', $subqueries), $this->_table, slMySqlConn::prepare($where_and_order, $params), $limit, $offset));
		
		if ($return)
			return $sql;
		else {
			$objs = array();
			foreach($this->_conn->select($sql)->assocs() as $record) {
				$o = $class->newInstance();
				$o->bind($record);
				$objs[] = $o;
			}
			
			return $objs;
		}
	}
	
	final function selectOne($where_and_order, $params = array()) {
		$return = SCOTTLIB_RETURN_SQL;
		$result = $this->select($where_and_order, $params, 1, 0, $return);
		if ($return)
			return $result; // sql
		else
			return (count($result) ? $result[0] : null);
	}
	
	final function getAll($order = '', $limit=250, $return = false) {
		return $this->select($order, array(), $limit, 0, $return);
	}
	
	final function delete($return = false) {
		if (!($id = $this->getId()) && !$return)
			throw new Exception("Cannot delete record: it isn't connected to the database.");
			
		$this->onBeforeDelete($id);
		$sql = slMySqlConn::prepare("DELETE FROM # WHERE #=%s", array($this->_table, $this->_idProperty, $id));
		if ($return)
			return $sql;
		else {
			$affected = $this->_conn->delete($sql);
			$this->onAfterDelete($id);
			return $affected;
		}
	}
	
	function onBeforeDelete($id) {}
	function onAfterDelete($id) {}
	
	final function bind($data = null, $exceptions = array()) {
		//echo '<pre>'.get_class($this).': '.print_r($exceptions, true).'</pre>';
		
		if ($data != null) {
			$this->onBeforeBind($data, $exceptions);
			$idDefinedByReflection = false;
			
			foreach($this->getProperties() as $prop) {
				if (get_class($prop) == 'ReflectionProperty' && !in_array($prop->getName(), $exceptions)) {
					$name = $prop->getName();
					$isRequired = (ReflectionUtils::hasPropertyAnnotation($this, $name, array('notNull', 'required')));
					$isBool = (ReflectionUtils::hasPropertyAnnotation($this, $name, array('boolean', 'bool')));
					if (ReflectionUtils::hasPropertyAnnotation($this, $name, 'id'))
						$idDefinedByReflection = true;
						
					
					//echo "<div>$name</div>";
					
					// make sure we have a default value for boolean in $data
					if ($isBool && (!array_key_exists($name, $data) || !strlen($data[$name])))
						$data[$name] = false;
						
						
					if (array_key_exists($name, $data)) {
						$value = $data[$name];
						
						// FIXME: This is probably not enough to logically process possible boolean values
						if ($isBool && !is_bool($value)) 
							$value = ord($value);
						
						$this->$name = $value;
						
					}
				}
			}
			
			if (!$idDefinedByReflection) {
				if (array_key_exists('id', $data))
					$this->id = $data['id'];	
			}
			
			$this->onAfterBind($data, $exceptions);
		}
	}
	
	function onBeforeBind($data, $exceptions) {}
	function onAfterBind($data, $exceptions) {}
	
	final function validate() {
		$this->onBeforeValidate();
		foreach($this->getProperties() as $prop) {
			if (get_class($prop) == 'ReflectionProperty') {
				$name = $prop->getName();
				
				$isRequired = (ReflectionUtils::hasPropertyAnnotation($this, $name, array('notNull', 'required')));
				$isBool = (ReflectionUtils::hasPropertyAnnotation($this, $name, array('boolean', 'bool')));
				$isNumeric = (ReflectionUtils::hasPropertyAnnotation($this, $name, array('float', 'double', 'integer')));
				
				//echo "<div>$name</div>";
				
				$value = $prop->getValue($this);
				
				// FIXME: This is probably not enough to logically process possible boolean values
				if ($isBool && !is_bool($value)) 
					$value = ord($value);
				
				if (!strlen($value) && $isRequired)
					$this->addError($name, 'required', 'Required');
					
				if ($isNumeric && !empty($value) && !is_numeric($value))
					$this->addError($name, 'numeric', 'Must be a number');
			}
		}
		$this->onAfterValidate();
		
		return !$this->hasErrors();
	}
	
	function onBeforeValidate() {}
	function onAfterValidate() {}
	
	final function bindAndValidate($data = null, $exceptions = array()) {
		$this->bind($data, $exceptions);
		return $this->validate();
	}
		
	
	/**
 	 * @return A count of errors when binding errors have occurred; otherwise, false.
	 */
	final function hasErrors($property = null) {
		return ($property ? ( array_key_exists($property, $this->_errors) ? count($this->_errors[$property] ) : false) : ( count($this->_errors) ? count($this->_errors) : false ));
	}
	
	/**
	 * @param String $property The name of a property of this method
     * @return When $property is not null, Array(stdClass) when error(s) are found for $property; otherwise, false.  When $property is null, returns all errors.
	 */
	final function getErrors($property = null) {
		return ($property ? ( array_key_exists($property, $this->_errors) ? $this->_errors[$property] : false ) : $this->_errors);
	}
	
	final function clearErrors() {
		$this->_errors = array();	
	}
	
	final function addError($property, $code = null, $defaultMessage = '') {
		if (!array_key_exists($property, $this->_errors))
			$this->_errors[$property] = array();

		if (!is_array($code))
			$code = array($code);
			
		foreach ($code as $c) {
			$error = new stdClass();
			$error->code = $c;
			$error->defaultMessage = $defaultMessage;
			$this->_errors[$property][] = $error;
		}
	}
	
	function toArray() {
		$data = array();
		foreach($this->getProperties() as $prop) {
			$name = $prop->getName();
			$value = $this->$name;
			
			// FIXME: this is a really stupid way to handle nested data:
			if (is_array($value) || is_object($value))
				$value = serialize($value);

			$data[$name] = $value;
		}	
		
		$data[$this->_idProperty] = $this->getId();
		return $data;
	}
	
	function serialize() {
		$data = $this->toArray();
		return serialize($data);	
	}
	
	function load($id) {
		
		$subqueries = array();
		$subqueries[] = '*';
		
		foreach($this->getProperties() as $prop) {
			if (get_class($prop) == 'ReflectionProperty') {
				if ($q = ReflectionUtils::getPropertyAnnotation($this, $prop->getName(), 'subquery')) {
					$prepared = slMySqlConn::prepare($q, array('tableName' => $this->getTableName()));
					$sql = slMySqlConn::prepare("( %p ) AS #", array($prepared, $prop->getName()));
					$subqueries[] = $sql; 
				}
			}
		}
		
		$sql = slMySqlConn::prepare("SELECT %p FROM # WHERE #=%s", array(join(',', $subqueries), $this->_table, $this->_idProperty, $id));
		
		$this->onBeforeLoad($id);
		
		$result = $this->_conn->select($sql);
		
		if ($result->count() > 1)
			throw new Exception("Too many records returned for load: ".count($rows));
		else if ($result->count() == 0)
			throw new Exception("No records found with id [$id]");

		foreach($result->assocs() as $record)  // there will be only one
			$this->bind($record);
			
		$this->onAfterLoad($id);
		
		return $this;
	}
	
	function onBeforeLoad($id) {}
	function onAfterLoad($id) {}
	
	function deleteAll($return = false) {
		$sql = slMySqlConn::prepare("DELETE FROM #", $this->_table);
		return ($return ? $sql : $this->_conn->execute($sql)); 		
	}
	
	final static function drop(slObject $obj, $conn = null, $return = false) {
		$sql = slMySqlConn::prepare("DROP TABLE IF EXISTS #", $obj->getTableName());
		
		if ($conn == null)
			$conn = slMySqlConn::instance();
		
		return ($return ? $sql : $conn->execute($sql));
	}
	
	final static function update(slObject $obj, $conn = null, $return = false) {
		throw new Exception("Schema updating is not yet implemented.");
	}
	
	final static function create(slObject $obj, $conn = null, $return = false) {
		
		$cache = ReflectionUtils::getCachedReflection($obj);
		$class = $cache['class'];
		
		$tableName = $obj->getTableName();
			
		$sql = array();
		
		$sql[] = slMySqlConn::prepare("CREATE TABLE IF NOT EXISTS # (", $tableName);
		
		$relationships = array();
		
		$fieldDefs = array();
		
		$idDefinedByReflection = false;
		
		foreach($cache['properties'] as $prop) {
			
			if (ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'id'))
				$idDefinedByReflection = true;
			
			// skip transient fields:
			if (ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), array('transient', 'subquery'))) continue;
			
			$fieldDef = '/* field name: */ # /* data type: */ %p /* unsigned: */ %p /* zerofill: */ %p /* null: */ %p /* auto_increment: */ %p /* unique: */ %p /* primary: */ %p /* comment: */ %p %p';
			
			$settings = array();
			
			$useAutoIncrement = ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'id') && !ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'noAutoIncrement');
			
			/* field name: */ 
				$settings[] = ($fieldName = ReflectionUtils::getPropertyAnnotation($obj, $prop->getName(), 'fieldName') ? $fieldName : $prop->getName());
			
			/* data type: */ 
			// FIXME: complete this with all available MySQL types
				if ($useAutoIncrement)
					$settings[] = 'BIGINT';
				else if (ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'boolean'))
					$settings[] = 'BIT';
				else if (ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'float'))
					$settings[] = 'FLOAT';
				else if (ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'double'))
					$settings[] = 'DOUBLE';
				else if (ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'integer'))
					$settings[] = 'INTEGER';
				else if (ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'date'))
					$settings[] = 'DATETIME';
				else if (ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'text'))
					$settings[] = 'TEXT';
				else if (ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'bigtext'))
					$settings[] = 'BIGTEXT';
				else if (ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'blob'))
					$settings[] = 'BLOB';
				else if (ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'longblob'))
					$settings[] = 'LONGBLOB';
				else if ($exactly = ReflectionUtils::getPropertyAnnotation($obj, $prop->getName(), 'exactly'))
					$settings[] = "CHAR($exactly)";
				else if ($maxLength = ReflectionUtils::getPropertyAnnotation($obj, $prop->getName(), 'maxLength'))
					$settings[] = "VARCHAR($maxLength)";
				else if ($varchar = ReflectionUtils::getPropertyAnnotation($obj, $prop->getName(), 'varchar'))
					$settings[] = "VARCHAR($varchar)";
				else
					$settings[] = 'VARCHAR(1024)';
			
			/* unsigned: */ $settings[] = (ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'unsigned') ? 'UNSIGNED' : '');
					
			/* zerofill: */ $settings[] = (ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'zerofill') ? 'ZEROFILL' : '');
					
			/* null: */ $settings[] = (ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'notNull') ? 'NOT NULL' : 'NULL');
					
			/* auto increment: */ $settings[] = ($useAutoIncrement ? 'AUTO_INCREMENT' : '');
					
			/* unique: */ $settings[] = (ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'unique') ? 'UNIQUE KEY' : '');
					
			/* primary: */ $settings[] = (ReflectionUtils::hasPropertyAnnotation($obj, $prop->getName(), 'id') ? 'PRIMARY KEY' : '');
					
			/* comment: */ 
				if ($comment = ReflectionUtils::getPropertyAnnotation($obj, $prop->getName(), 'comment')) {
					$settings[] = 'COMMENT';
					$settings[] = "'".mysql_escape_string($comment)."'";	
				}
				else {
					$settings[] = '';
					$settings[] = '';
				}
			
			$fieldDefs[] = slMySqlConn::prepare($fieldDef, $settings);		
			
			if (ReflectionUtils::getPropertyAnnotation($obj, $prop->getName(), 'indexed') && !ReflectionUtils::getPropertyAnnotation($obj, $prop->getName(), 'unique') && !ReflectionUtils::getPropertyAnnotation($obj, $prop->getName(), 'id')) 
				$fieldDefs[] = slMySqlConn::prepare('INDEX(#)', $tableName);
		}
		
		if (!$idDefinedByReflection) {
			$fieldDefs[] = "`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY"; 
		}
		
		/*
		foreach($cache['methods'] as $method) {
			
			$name = $method->getName();
			
			if ($oneToMany = ReflectionUtils::getMethodAnnotation($obj, $name, 'oneToMany')) {
				$manyTableName = (self::hasClassAnnotation($oneToMany, 'table') ? self::getClassAnnotation($oneToMany, 'table') : self::uncap($oneToMany));
				$relTableName = (self::hasClassAnnotation($obj, 'table') ? self::getClassAnnotation($obj, 'table') : self::uncap(get_class($obj))).'_'.$manyTableName;
				$ddl = slMySqlConn::prepare("CREATE TABLE IF NOT EXISTS # ( `oneId` BIGINT UNSIGNED, `manyId` BIGINT UNSIGNED, INDEX(`oneId`), INDEX(`manyId`), INDEX(`oneId`,`manyId`) ) type=InnoDB", $relTableName);
				$relationships[] = $ddl;
			}
			
			else if ($manyToOne = self::getMethodAnnotation($obj, $name, 'manyToOne')) {
				$oneTableName = (self::hasClassAnnotation($manyToOne, 'table') ? self::getClassAnnotation($manyToOne, 'table') : self::uncap($manyToOne));
				$relTableName = $oneTableName.'_'.$obj->_table;
				$ddl = slMySqlConn::prepare("CREATE TABLE IF NOT EXISTS # ( `oneId` BIGINT UNSIGNED, `manyId` BIGINT UNSIGNED, INDEX(`oneId`), INDEX(`manyId`), INDEX(`oneId`,`manyId`) ) type=InnoDB", $relTableName);
				$relationships[] = $ddl;
			}
			
			else if ($oneToOne = self::getMethodAnnotation($obj, $name, 'oneToOne')) {
				$oneTableName = (self::hasClassAnnotation($oneToOne, 'table') ? self::getClassAnnotation($oneToOne, 'table') : self::uncap($oneToOne));
				$fieldDefs[] = slMySqlConn::prepare("# BIGINT UNSIGNED, INDEX(#)", array($oneTableName."Id", $oneTableName."Id"));
			}
			
			else if ($manyToMany = self::getMethodAnnotation($obj, $name, 'manyToMany')) {
				$manyTableName = (self::hasClassAnnotation($manyToMany, 'table') ? self::getClassAnnotation($manyToMany, 'table') : self::uncap($manyToMany));
				$names = array($obj->_table, $manyTableName);
				sort($names);
				$relTableName = "$names[0]_$names[1]";
				$ddl = slMySqlConn::prepare("CREATE TABLE IF NOT EXISTS # ( # BIGINT UNSIGNED, # BIGINT UNSIGNED, INDEX(#), INDEX(#), INDEX(#,#) ) type=InnoDB", array($relTableName, $names[0].'Id', $names[1].'Id', $names[0].'Id', $names[1].'Id', $names[0].'Id', $names[1].'Id'));
				$relationships[] = $ddl;
			}
			
		}
		*/
		
		$sql[] = join(",\n", $fieldDefs);
		
		$tableType = ($tableType = ReflectionUtils::getClassAnnotation($obj, 'tableType') ? $tableType : 'InnoDB');
		
		$sql[] = ") type=$tableType";
		
		$sql = join("\n", $sql);
		
		// echo '<pre>'.$sql.'</pre>';
		
		if (!$return) {
			if ($conn == null)
				$conn = slMySqlConn::instance();

			$affected = $conn->execute($sql);

			foreach($relationships as $r)
				$affected += $conn->execute($r);
				
			return $affected;
		}
		
		else
			return $sql.(count($relationships) ? "\n".join("\n", $relationships) : "");
			
	}
	
	/**
	 * Properly escapes property value $property for embedding in HTML using PHP htmlentities function
	 * @param string $property The name of the value to retrieve
	 * @param boolean $return
	 * @return When $return is true, returns the value; otherwise, echos it to the output
	 */
	final function html($property, $return = false) {
		if ($return)
			return htmlentities($this->$property);
		else
			echo htmlentities($this->$property);
	}
	
	final function val($property, $return = false) {
		if ($return)
			return $this->$property;	
		else
			echo $this->$property;
	}
	
	final function money($property, $return = false) {
		$value = $this->$property;
		if ($value) {
			$value = number_format($this->$property, 2);
			if ($return) 
				$value;
			else
				echo $value;
		}
		else {
			if ($return)
				return null;
			else
				echo '';
		}		
	}
	
	final function checked($property) {
		echo ($this->$property ? 'checked="checked"' : '');	
	}
	
	final function selected($property, $value = null) {
		echo ($this->$property == $value ? 'selected="selected"' : '');	
	}
	
	final function error($property) {
		if (($errors = $this->getErrors($property)) !== false) {
			// print_r($errors);
			$error = $errors[0];
			// FIXME: implement error code translation here	
			echo "<span class=\"error\">$error->defaultMessage</span>";
		}
	}

} // end slObject

